深入理解Java虚拟机(六): 类文件结构
点击上方"淼淼之森",选择"关注"公众号
优秀文章,第一时间收到!
Knowledge Sharing
知识分享
现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文有用,希望你能将它分享,让更多的人看到。
推荐阅读
1、神站推荐
2、资源汇总整理分享
类文件结构
Class类文件的结构
概念
Class文件是一组以8位字节为基础单位的二进制流
按顺序整齐排列
没有分隔符
排序方式:高位在前
Big-Endian: 最高位字节在地址最低位、最低位字节在地址最高位。
Little-Endian:相反。
存储方式
类似于C语言结构体的伪结构来存储
只有两种数据类型:
无符号数:属于基本数据类型
u1 : 1个字节
u2 : 2个字节
u4 : 4个字节
u8 : 8个字节
表:多个无符号数或其他表作为数据项构成的复合数据类型
以"_info"结尾。
描述有层次关系的复合结构数据
整个Class文件本质上就是一张表
魔数
魔数
每个Class文件的头4个字节。
确定这个文件是不是一个能被虚拟机接受的Class文件
Java中的值为:0xCAFEBABE --> 咖啡宝贝?
Class文件的版本
紧接着的4个字节
第5个和第6个:次版本号(Minor Version)
第7个和第8个:主版本号(Major Version)
满足向下兼容,即高版本兼容低版本的Class文件
常量池
紧接着版本号
Class文件中的资源仓库
常量池容量计数值(constant_pool_count)
常量池中常量数量不确定,所以需要一个计数
是u2类型的数据
从1开始而不是从0开始计数
设计者将0位做特殊考虑,特殊情况下,有些指向常量池的索引数据要表达“==不引用任何常量池项目==”的意义。
常量池容量(偏移地址:0x00000008)为十六进制数0x0016,十进制22,表示常量池中有21项常量,索引1~21。
主要存放类型
字面量(Literal):如文本字符串、final常量等
符号引用(Symbolic References)
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
14种常量池的每个常量表
CONSTANT_Utf8_info 1 UTF-8
包含的类型:u1 tag 1 标志位
u2 length 1 utf-8字符串的长度
u1 bytes length 真正的字符串内容
CONSTANT_Integer_info 3 整型
CONSTANT_Float_info 4 浮点型
CONSTANT_Long_info 5 长整型
CONSTANT_Double_info 6 双精
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串型
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 动态方法调用点
java代码示例
PS E:\> javac .\TestClass.java
PS E:\> javap -verbose .\TestClass.class
Classfile /E:/TestClass.class
Last modified 2018-8-9; size 293 bytes
MD5 checksum a364fba0fc6304868b1a4cc1a3ba3729
Compiled from "TestClass.java"
public class org.laowang.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
{
public org.laowang.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
}
SourceFile: "TestClass.java"
访问标志(access_flags)
紧接着常量池
两个字节
总共16位,当前只定义了其中8个,未使用的一律为0
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_FINAL | 0x0010 | final |
ACC_SUPER | 0x0020 | invokespecial(JDK1.0.2之后编译出来的类这个标志欧必须为真) |
ACC_INTERFACE | 0x0400 | abstract(接口或者类) |
ACC_SYNTHETIC | 0x1000 | 并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
类索引(this_class)、父类索引(super_class)与接口索引(interfaces)集合
紧接着访问标志
u2类型(this_class、super_class)
u2类型集合(interfaces)
各索引的含义
这三项数据来确定类的继承关系
类索引(this_class)用来确定类全限定名
父类索引(super_class)用来确定父类的全限定名
接口索引描述类实现的接口,按从左到右的顺序排列。
父类索引(super_class)除了java.lang.Object外,其他的类都不为0。
字段表集合(field_info)
描述接口或类中声名的变量
包含:
作用域(public private protected)
实例变量还是类变量(static)
可变性(final)
并发可见性(volatile)
序列化(transient)
字段数据类型
字段名称
全限定名,相当于类的全路径,只是把.换成/,如java/lang/Object
简单名称,方法或属性名的简写,如inc()方法和m属性,则简单名称为inc和m
描述符:
描述字段的数据类型、方法的参数列表和返回值。
基本数据类型和void都用一个大写字母来表示,对象类型用L加全限定名来表示
标识名称 | 含义 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型 |
数组描述符:
一维数组:String[] --> [Ljava/lang/String
二维数组:String[][] --> [[Ljava/lang/String
方法描述符:
参数列表在前,返回值在后的顺序
参数列表放在()内,如:
void inc() 描述符为()V
int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex) 描述为 ([CII[CIII)I
方法表集合
包含
访问标志(access_flags)
名称索引(name_index)
描述符索引(descriptor_index)
属性表集合(attributes)
方法表集合中没有方法里面的代码,Java代码存在属性表集合中名为“Code”的属性里面
重写和重载的原理
重写(Override):如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但是,有可能出现编译器自动添加的方法,如类构造器
方法和实例构造器方法。重载(Overload):要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。因为返回值不会包含在特征签名中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有的方法进行重载。但在Class文件格式中,如果一个方法有相同的名称和特征签名,但返回值不同,也可以合法共存于同一个Class文件中。
特征签名:一个方法中各个参数在常量池中的字段符号引用的集合。
属性表集合
描述某些场景的专有信息
没有太多顺序、长度、内容的限值
虚拟机规范预定于的属性
属性名 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 只有当一个类是局部类或匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6新增,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK1.5新增,支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名 |
SourceDebugExtension | 类文件 | JDK1.6新增,用于存储额外的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVarialbleTypeTable | 类 | JDK1.5新增,使用特征签名代替描述符 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5新增,为动态注解提供支持,指明哪些注解是运行时可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5新增,指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5新增,与RuntimeVisibleAnnotations类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5新增,与RuntimeInvisibleAnnotations类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK1.5新增,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7新增,用于保存invokedynamic指令引用的引导方法限定符 |
Code属性
Java编译器处理后的字节码指令存储在Code属性内
Code属性出现在方法表的属性集合中
并非所有的方法表都必须存在这个属性
结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index:指向CONSTANT_UTF8_info型常量的索引 | 1 |
u4 | attribute_length:属性值的长度 | 1 |
u2 | max_stack:操作数栈的最大深度 | 1 |
u2 | max_locals:局部变量表所需的存储空间 | 1 |
u4 | code_length:编译后的字节码指令长度 | 1 |
u1 | code:编译后的字节码指令流 | code_length |
u2 | exception_table_length:异常表长度 | 1 |
excpetion_info | exception_table:异常表内容 | excpetion_table_length |
u2 | attributes_counts: 属性数量 | 1 |
attribute_info | attributes: 属性内容 | attributes_counts |
Code属性是Class文件中最重要的一个属性,如果把Class文件就分为代码(Code)和元数据(Metadata)两部分。那么Code可以解释为描述代码,元数据解释为其他数据。
attribute_name_index:固定为“Code”,表示该属性的名字
attribute_length:由于属性名和属性长度一共6个字节(u2+u4),所以属性值的长度就是整个属性表的长度减去这两个属性。
max_stack:在方法执行的任意时刻,操作数栈都不会超过这个长度。虚拟机运行的时候根据这个值来分配栈帧中的操作栈深度。
max_locals:单位是Slot,是虚拟机为局部变量分配内存的最小单位。(参考对应01、虚拟机 VM Stack)
Slot可重用:并不是方法中使用了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals。当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用。
code_length:code_length代表编译后的字节码长度,code是存储字节码指令的一系列字节流。
exception_table_length:不是必须存在的。
this关键字
在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。
它的实现其实非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。
因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量。
局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,而对静态方法无效。
例:
有效:
package org.laowang.clazz;
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
无效:
package org.laowang.clazz;
public class TestClass{
private static int m;
public static int inc(){
return m+1;
}
}
Exceptions属性
列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
LineNumberTable属性
描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
不是运行时必须的属性,但默认会生成到Class文件中
可以在Javac中分别使用-g:none或-g:lines来取消或者要求生成这项信息。
如果不生成这个属性,最大的影响是当抛出异常时,堆栈中部会显示出错的行号,并且在调试的时候,也无法按照源码来设置断点。
LocalVariableTable属性
描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系
不是运行时必须的属性,但是会默认生成到Class文件中。
可以在Javac中分别使用-g:none或-g:vars来取消或者要求生成这项信息。
如果不生成这个属性,最大的影响是当其他人引用这个方法时,所有的参数名称都回丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,但对程序运行没有影响。
SourceFile属性
记录生成这个Class文件的源码文件名称。
可选的属性。
可以在Javac中分别使用-g:none或-g:source来关闭或者要求生成这项信息。
如果不开启,最大的影响是内部类抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。
ConstantValue属性
通知虚拟机自动为静态变量赋值。
如果是非静态变量,赋值是在
方法中。如果是静态:
如果是被final static修饰的一个变量,并且是基本类型或者是String类型,就要生成ConstantValue来进行初始化。
如果没有被final修饰或者不是基本类型及字符串,则会选择在
方法中进行初始化。
InnerClasses属性
记录内部类与宿主类之间的关联。
如果一个类定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
Deprecated及Synthetic属性
Deprecated: 使用@Deprecated注解进行设置,表示不再推荐使用,过时了。
Synthetic:表示字段或方法并不是由Java源码直接产生的,而是由编译器添加的。
StackMapTable属性*
Signature属性*
BootstrapMethods属性*
-END-
本文参考《深入理解Java虚拟机》(第二版)
关注公众号"淼淼之森";后台回复序号“666”获取超多精彩哦。